6 NumPy 数组运算
6.1 引言向量化运算的威力
NumPy的核心优势在于其强大的向量化运算能力。与Python循环相比,NumPy运算在底层使用优化的C/Fortran代码,可以提供数十倍甚至上百倍的性能提升。在金融数据处理中,这种性能优势使得复杂计算成为可能。
6.2 本章学习目标
通过本章学习,你将能够:
- 使用
.max()、.min()、.mean()、.var()、.std()计算股价与涨跌幅数组的基础统计量,并用.round()控制输出小数位 - 说明
axis=0与axis=1在多维数组统计中的方向含义,并用.cumsum()计算累积收益率 - 使用元素级算术与
np.log()、np.exp()、np.sqrt()、np.abs()、np.floor()、np.ceil()完成收益率、复利与波动率计算 - 使用
np.intersect1d()等集合运算比较两个投资组合的持仓差异,用布尔索引与&、|、~完成多条件股票筛选 - 使用
np.sort()、np.argsort()、argmax/argmin、np.where()完成排序、定位与条件替换,并用向量化运算替代 Python 循环
6.3 统计运算
6.3.1 基本统计量
NumPy提供了丰富的统计函数,这些函数可以直接应用于整个数组或指定轴(axis)。
任务要求:已知中国建筑 8 月 18 个交易日的收盘价数组 stock_p 与对应涨跌幅数组 stock_return(单位:%),依次输出三项内容:最高股价 stock_p.max()、保留三位小数的平均涨跌幅 stock_return.mean().round(3)、保留三位小数的方差与标准差 stock_return.var().round(3) 与 stock_return.std().round(3)。请将代码原样输入教学平台(注释除外),判定以平台为准。
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np # 导入NumPy数值计算库
# 已知数据
stock_p=np.array([4.98,5.02,4.95,4.91,4.98,4.92,4.88,4.92,4.88,4.82,4.85,4.89,4.91,4.86,4.84,4.92,5.01,5.04])
# 创建NumPy数组stock_return
stock_return=np.array([-0.7968,1.4141,0.8147,-1.4056,1.2195,0.8197,-0.813,0.8197,1.2448,-0.6186,-0.818,-0.4073,1.0288,0.4132,-1.626,-1.7964,-0.5952,-1.5625])
# 1. 计算股票的最高价并输出
p_max=stock_p.max()
print(f"中国建筑8月最高股价为{p_max}") # 输出中国建筑8月最高股价为
# 2. 计算股票的平均涨跌幅,使用round()函数保留三位小数并输出
return_avg=stock_return.mean().round(3)
print(f"平均涨跌幅为{return_avg}") # 输出平均涨跌幅为
# 3. 计算股票涨跌幅的方差和标准差,使用round()函数并保留3位小数
return_var=stock_return.var().round(3)
return_std=stock_return.std().round(3) # 计算标准差并四舍五入
print(f"涨跌幅的方差{return_var},标准差{return_std}") # 输出涨跌幅的方差预期输出(本机 Python 实际运行结果,具体以平台运行结果为准):
中国建筑8月最高股价为5.04
平均涨跌幅为-0.148
涨跌幅的方差1.169,标准差1.081
判读要点:平均涨跌幅为 -0.148(%),说明当月整体小幅下跌;方差与标准差按 NumPy 默认的总体口径(ddof=0)计算,若改用样本口径 ddof=1,结果会略微变大。
代码深度解析:
.max()和.min():- 返回数组的最大/最小值
- 支持
axis参数指定沿哪个轴计算 - 等价于
np.max(arr)和np.min(arr)
.mean()(算术平均):mean = Σx_i / n- 受极端值影响
- 适用于对称分布数据
.var()(方差):# 总体方差(NumPy默认,ddof=0) var_pop = Σ(x_i - mean)² / n # 样本方差,无偏估计(ddof=1) var_sample = Σ(x_i - mean)² / (n-1)ddof: Delta Degrees of Freedom(自由度调整)- 金融分析常用样本方差(ddof=1)
- 另注意:Pandas 的 std()/var() 默认 ddof=1(样本口径),与 NumPy 默认 ddof=0 不同,同一组数据两库结果会差一点。
.std()(标准差):std = √variance- 与原始数据同单位,更易解释
- 金融含义: 波动率的度量,风险的核心指标
6.3.2 多维数组的统计运算
# =============================================================================
# 题目:沿指定轴计算统计量
# =============================================================================
# 本任务演示如何对多维数组沿指定轴进行统计运算
# 场景:分析多只股票多日的收益率数据,计算不同维度的统计量
# ==================== 创建多维数据 ====================
# 多只股票多日收益率:3只股票 × 4个交易日的收益率矩阵
# 每行代表一只股票,每列代表一个交易日
returns = np.array([
[0.05, 0.03, -0.02, 0.04], # 股票A:4个交易日的收益率
[0.02, 0.01, 0.03, -0.01], # 股票B:4个交易日的收益率
[-0.01, 0.02, 0.01, 0.05] # 股票C:4个交易日的收益率
]) # 形状为(3, 4)的二维数组,3行4列
# ==================== 沿axis=0计算每日的平均收益率 ====================
# axis=0:沿垂直方向(跨行)计算,即对每列计算均值
# 这里跨3只股票计算每个交易日的市场平均收益率
daily_avg = returns.mean(axis=0) # 对每列求均值,返回长度为4的一维数组
print(f"每日平均收益率: {daily_avg}")
# 输出解读:返回4个值,分别对应4个交易日的市场平均收益率
# 金融含义:市场平均收益率反映整体市场的表现,可用于计算市场因子
# 应用:如果某日市场平均收益率为正,说明当日市场整体上涨
# ==================== 沿axis=1计算每只股票的平均收益率 ====================
# axis=1:沿水平方向(跨列)计算,即对每行计算均值
# 这里跨4个交易日计算每只股票的平均收益率
stock_avg = returns.mean(axis=1) # 对每行求均值,返回长度为3的一维数组
print(f"每只股票平均收益率: {stock_avg}")
# 输出解读:返回3个值,分别对应股票A、B、C的平均收益率
# 金融含义:个股平均收益率反映该股票在统计期内的典型表现
# 应用:比较不同股票的平均收益率,筛选表现较好的股票
# ==================== 计算累积收益率 ====================
# .cumsum(axis=1):沿axis=1方向计算累积和
# 累积和用于计算多期收益率的累积效果
cumulative_returns = returns.cumsum(axis=1) # 对每行计算累积和,返回形状为(3, 4)的数组
print(f"\n累积收益率:\n{cumulative_returns}")
# 输出解读:每行的第i个值 = 该行前i个元素的和
# 例如股票A的第3个值 = 0.05 + 0.03 + (-0.02) = 0.06,表示前3日累积收益率为6%
# 金融含义:累积收益率反映投资一段时间的总收益
# 应用:计算持仓的累积收益,评估投资策略的历史表现axis参数理解:
axis=0: 垂直方向(跨行),对每列计算axis=1: 水平方向(跨列),对每行计算axis=None: 对所有元素计算(默认)
金融应用:
axis=0: 多只股票同一时间的表现axis=1: 单只股票的时间序列特征
6.4 数学运算
6.4.1 算术运算
NumPy支持元素级的算术运算,这是向量化的核心。
# =============================================================================
# 题目:NumPy数组的算术运算
# =============================================================================
# 本任务演示NumPy数组的元素级算术运算
# 场景:金融场景中的价格调整、价差计算、持仓价值计算、收益率计算
# ==================== 创建基础数据 ====================
# 基础算术:创建股价序列
prices = np.array([10, 20, 30, 40, 50]) # 5个交易日的股价数据,单位:元
# ==================== 加法:价格调整 ====================
# 创建调整数组,模拟价格调整(如分红、拆股等)
adjustment = np.array([1, 2, 3, 4, 5]) # 每天的价格调整幅度
new_prices = prices + adjustment # 对应元素相加,得到调整后的价格
print(f"调整后价格: {new_prices}")
# 输出解读:[11, 22, 33, 44, 55],每个元素 = 对应位置的price + adjustment
# 金融应用:股票除权除息后的价格调整,或者计算复权价格
# ==================== 减法:价差计算 ====================
# prices[1:]:从索引1开始到最后的切片,即[20, 30, 40, 50]
# prices[:-1]:从开始到倒数第二个的切片,即[10, 20, 30, 40]
# 相减得到相邻两天的价格差
price_diff = prices[1:] - prices[:-1] # 计算相邻价格的差值
print(f"价格变化: {price_diff}")
# 输出解读:[10, 10, 10, 10],表示每天价格上涨10元
# 金融含义:价格差反映价格的绝对变化,用于计算价格动量
# 应用:技术分析中的价格变化率、MACD指标等
# ==================== 乘法:计算持仓价值 ====================
# 创建持仓数量数组
shares = np.array([100, 200, 150]) # 3只股票的持仓数量
# prices[:3]:取prices的前3个元素[10, 20, 30]
position_value = prices[:3] * shares # 对应元素相乘,计算每只股票的持仓价值
print(f"\n持仓价值: {position_value}")
# 输出解读:[1000, 4000, 4500],分别表示3只股票的持仓价值
# 金融应用:持仓价值 = 股价 × 持仓数量,用于计算投资组合的市值
# 注意:两个数组形状必须相同,或可以广播
# ==================== 除法:计算收益率 ====================
# 简单收益率公式:return = (P_t - P_{t-1}) / P_{t-1}
# prices[1:]:当期价格,prices[:-1]:上期价格
returns = (prices[1:] - prices[:-1]) / prices[:-1] # 计算日收益率
print(f"\n简单收益率: {returns}")
# 输出解读:[1.0, 0.5, 0.33, 0.25],分别表示每天的收益率(小数形式)
# 金融含义:第1天收益率 = (20-10)/10 = 100%,第2天 = (30-20)/20 = 50%
# 应用:计算历史收益率,用于评估股票的历史表现和风险
# ==================== 幂运算:复利计算 ====================
# np.arange(1, 6):生成整数序列[1, 2, 3, 4, 5],代表年数
# (1 + 0.05):1 + 年化收益率5%
# **:幂运算符,计算复利因子
compound = (1 + 0.05) ** np.arange(1, 6) # 计算1到5年的复利因子
print(f"\n5年复利因子: {compound}")
# 输出解读:[1.05, 1.1025, 1.1576, 1.2155, 1.2763]
# 金融含义:第1年末本金变为1.05倍,第2年末变为1.1025倍,依此类推
# 应用:复利计算、未来价值计算、投资回报预测运算符对应关系:
| 运算符 | NumPy函数 | 描述 |
|---|---|---|
+ |
np.add |
加法 |
- |
np.subtract |
减法 |
* |
np.multiply |
乘法 |
/ |
np.divide |
除法 |
// |
np.floor_divide |
整除 |
% |
np.mod |
取模 |
** |
np.power |
幂运算 |
6.4.2 数学函数
NumPy提供了完整的数学函数库,这些函数都是向量化的。
# =============================================================================
# 题目:NumPy数学函数在金融计算中的应用
# =============================================================================
# 本任务演示NumPy常用数学函数在金融场景中的应用
# 场景:对数收益率计算、复利计算、波动率计算、价格取整
# ==================== 创建示例数据 ====================
# 示例数据:5个交易日的股价数据
prices = np.array([100, 105, 98, 102, 110]) # 股价序列,单位:元
# ==================== 1. 对数收益率计算(金融常用) ====================
# np.log(prices):计算价格的自然对数,ln(prices)
# np.diff():计算相邻元素的差值,a[n] - a[n-1]
# 对数收益率公式:r_log = ln(P_t) - ln(P_{t-1}) = ln(P_t / P_{t-1})
log_returns = np.diff(np.log(prices)) # 计算对数收益率
print(f"对数收益率: {log_returns}")
# 输出解读:返回4个值,表示4个交易日的对数收益率
# 金融含义:对数收益率具有时间可加性,多期收益率可以直接相加
# 优势:相比简单收益率,对数收益率更接近正态分布,便于统计建模
# 应用:连续复利模型、Black-Scholes期权定价模型
# ==================== 2. 指数函数:复利计算 ====================
# 创建年化收益率数组:4只股票的年收益率
annual_returns = np.array([0.05, 0.06, 0.04, 0.07]) # 小数形式,5%、6%、4%、7%
# annual_returns * 5:计算5年的累积收益率
# np.exp():指数函数,e^x,用于连续复利计算
# cumulative_5yr = e^(r * 5) - 1,连续复利公式
cumulative_5yr = np.exp(annual_returns * 5) - 1 # 计算5年累计收益率
print(f"\n5年累计收益率: {cumulative_5yr}")
# 输出解读:返回4个值,表示4只股票5年后的累计收益率
# 金融含义:假设年化收益率为5%,5年后累计收益率 = e^(0.05*5) - 1 = 28.4%
# 应用:长期投资回报预测、养老金计算、财富增长规划
# ==================== 3. 平方和平方根:波动率计算 ====================
# np.std():计算标准差,衡量收益率波动程度
volatility = np.std(annual_returns) # 计算年化收益率的标准差
# ** 2:平方运算,计算方差
variance = volatility ** 2 # 方差 = 标准差的平方
print(f"\n波动率: {volatility:.4f}")
print(f"方差: {variance:.4f}")
# 输出解读:波动率如0.0112表示年收益率的标准差为1.12个百分点
# 金融含义:波动率是风险的量化度量,波动率越大,风险越高
# 应用:投资组合风险管理、VaR(在险价值)计算、期权定价
# ==================== 4. 绝对值:计算收益的绝对变化 ====================
# np.diff(prices):计算相邻价格的差值
# np.abs():计算绝对值,将负数转为正数
abs_changes = np.abs(np.diff(prices)) # 计算价格变化的绝对值
print(f"\n价格绝对变化: {abs_changes}")
# 输出解读:返回正数,表示价格变化的幅度(不考虑方向)
# 金融应用:计算价格波动幅度,不考虑涨跌方向
# 区别:price_diff有正负(方向),abs_changes只有大小(幅度)
# ==================== 5. 取整函数 ====================
# 创建浮点数价格数组,模拟包含小数的股价
prices_float = np.array([100.56, 105.23, 98.87]) # 股价保留两位小数
# np.floor():向下取整,返回不大于x的最大整数
print(f"\n向下取整: {np.floor(prices_float)}")
# 输出解读:[100., 105., 98.],每个元素向下取整
# 应用:金融计算中需要保守估计时使用向下取整
# np.ceil():向上取整,返回不小于x的最小整数
print(f"向上取整: {np.ceil(prices_float)}")
# 输出解读:[101., 106., 99.],每个元素向上取整
# 应用:计算所需资金时向上取整,确保资金充足
# np.round():四舍五入到最接近的整数
print(f"四舍五入: {np.round(prices_float)}")
# 输出解读:[101., 105., 99.],按照四舍五入规则取整
# 应用:报价、显示价格时使用四舍五入
# 注意:round(0.5) = 0,采用"银行家舍入法"(向偶数舍入)补充说明:对数收益率的数学基础
在金融中,对数收益率(Log Return)优于简单收益率:
\[ r_{log} = \ln\left(\frac{P_t}{P_{t-1}}\right) = \ln(P_t) - \ln(P_{t-1}) \]
优势:
- 时间可加性: 多期收益率可以直接相加 \[ r_{log,1→3} = r_{log,1→2} + r_{log,2→3} \]
- 对称性: 正负收益对称(-50%和+50%的对数收益绝对值相等)
- 正态性: 更接近正态分布,便于统计推断
6.5 金融指标计算实例
6.5.1 案例股票风险收益分析
# =============================================================================
# 题目:综合运用NumPy进行股票风险收益分析
# =============================================================================
# 本任务综合运用NumPy的统计和运算函数,对股票进行全面的风险收益分析
# 场景:分析中国建筑某月的股价数据,计算各类风险和收益指标
# ==================== 创建股价数据 ====================
# 中国建筑8月股价数据(单位:元),共18个交易日
stock_p = np.array([4.98, 5.02, 4.95, 4.91, 4.98, 4.92, 4.88, 4.92,
4.88, 4.82, 4.85, 4.89, 4.91, 4.86, 4.84, 4.92,
5.01, 5.04]) # 一维数组,存储每日收盘价
# ==================== 计算日收益率 ====================
# stock_p[1:]:从第2个价格开始(第2天到第18天的价格)
# stock_p[:-1]:从第1个价格到倒数第2个(第1天到第17天的价格)
# 收益率公式:(当期价格 - 上期价格) / 上期价格
daily_returns = (stock_p[1:] - stock_p[:-1]) / stock_p[:-1] # 计算17个日收益率
# 输出解读:返回长度为17的数组,表示第2天到第18天的日收益率
# 金融含义:正收益率表示当天上涨,负收益率表示当天下跌
# ==================== 计算基本统计量 ====================
# .mean():计算日平均收益率
mean_return = daily_returns.mean() # 17个日收益率的算术平均
# 金融含义:表示平均每日的收益率水平,用于估计未来预期收益
# .std():计算日收益率的标准差
std_return = daily_returns.std() # 日波动率
# 金融含义:表示日收益率围绕均值波动的程度,是日风险的度量
# 年化波动率:将日波动率转换为年化波动率
# np.sqrt(252):根号252,252是一年的交易日数
# 公式:年化波动率 = 日波动率 × sqrt(252)
annualized_vol = std_return * np.sqrt(252) # 计算年化波动率
# 金融含义:假设日波动率持续一年,年化波动率约为多少
# 应用:不同资产的波动率比较、风险预算、风险控制
# ==================== 打印风险收益分析报告 ====================
print("=" * 50) # 打印分隔线
print("中国建筑8月风险收益分析") # 报告标题
print("=" * 50) # 打印分隔线
# 打印收益率统计信息
print(f"\n【收益率统计】")
print(f" 日平均收益率: {mean_return:.4%}") # 格式化为百分比,保留4位小数
print(f" 日收益率标准差: {std_return:.4%}") # 日波动率
print(f" 年化波动率: {annualized_vol:.4%}") # 年化波动率
# 输出解读:如日平均收益率-0.10%表示平均每日下跌0.10%
# 金融含义:负的平均收益率表明该月整体表现不佳
# ==================== 计算价格统计 ====================
# .max():计算最高价
max_price = stock_p.max() # 8月期间的最高股价
# .min():计算最低价
min_price = stock_p.min() # 8月期间的最低股价
# 价格区间:最高价 - 最低价
price_range = max_price - min_price # 计算8月价格波动区间
print(f"\n【价格统计】")
print(f" 最高价: {max_price:.2f}元") # 保留2位小数
print(f" 最低价: {min_price:.2f}元") # 保留2位小数
print(f" 价格区间: {price_range:.2f}元") # 价格波动幅度
# 金融应用:根据价格区间设置止损止盈位,如最低价附近设置止损
# ==================== 计算下行风险指标 ====================
# 布尔索引:筛选收益率为负的交易日
daily_returns < 0 # 返回布尔数组,True表示该日收益率为负
downside_returns = daily_returns[daily_returns < 0] # 提取所有负收益率
# 金融含义:下跌日的收益率数据,用于计算下行风险
# 下行波动率:只考虑下跌日的波动率
downside_risk = downside_returns.std() * np.sqrt(252) # 年化下行波动率
# 金融含义:下行风险只关注下跌时的波动,更符合投资者对风险的感知
print(f"\n【风险指标】")
print(f" 下跌日波动率: {downside_risk:.4%}") # 下行风险
# 输出解读:下行风险通常小于整体波动率,因为忽略了上涨日的波动
# 夏普比率:衡量单位风险的超额收益
# 公式:夏普比率 = (年化收益率 - 无风险利率) / 年化波动率
# mean_return * 252:日收益率年化
# 0.03:假设无风险利率为3%
sharpe_ratio = (mean_return * 252 - 0.03) / annualized_vol
print(f" 夏普比率(假设无风险利率3%): {sharpe_ratio:.2f}")
# 输出解读:夏普比率如-0.5,表示每承担1单位风险,超额收益为-0.5%
# 金融含义:夏普比率 > 1 为优秀,< 1 为一般,< 0 为较差(负值表示回报低于无风险利率)
# 应用:比较不同投资策略的风险调整后收益,筛选性价比高的投资6.6 集合运算
# =============================================================================
# 题目:NumPy集合运算在股票筛选中的应用
# =============================================================================
# 本任务演示NumPy的集合运算函数
# 场景:分析两个投资组合的持仓差异,找出共同持仓和独有持仓
# ==================== 创建投资组合数据 ====================
# 投资组合A的股票代码:包含4只股票
portfolio_a = np.array(['600519.SH', '000858.SZ', '600036.SH', '000002.SZ'])
# 投资组合B的股票代码:包含4只股票
portfolio_b = np.array(['600036.SH', '601318.SH', '000858.SZ', '600000.SH'])
# 注意:股票代码格式,上海交易所.SH,深圳交易所.SZ
# ==================== 交集:找出共同持仓 ====================
# np.intersect1d():计算两个数组的交集,返回共有的元素(已排序)
common = np.intersect1d(portfolio_a, portfolio_b) # 找出A和B都有的股票
print(f"共同持仓: {common}")
# 输出解读:['000858.SZ' '600036.SH'],两只组合共同持有这两只股票
# 金融应用:投资组合重叠度分析,避免过度集中风险
# ==================== 并集:找出所有股票 ====================
# np.union1d():计算两个数组的并集,返回所有不重复的元素(已排序)
all_stocks = np.union1d(portfolio_a, portfolio_b) # 合并A和B的所有股票,去重
print(f"所有股票: {all_stocks}")
# 输出解读:返回6只股票(4+4-2=6),因为2只是共同的
# 金融应用:构建分散化的投资池,确保股票多样性
# ==================== 差集:找出独有持仓 ====================
# np.setdiff1d(ar1, ar2):计算在ar1中但不在ar2中的元素
# 找出A组合独有(在A中但不在B中)的股票
unique_a = np.setdiff1d(portfolio_a, portfolio_b) # A特有的股票
print(f"A组合独有: {unique_a}")
# 输出解读:['000002.SZ' '600519.SH'],这两只只在A组合中
# 找出B组合独有(在B中但不在A中)的股票
unique_b = np.setdiff1d(portfolio_b, portfolio_a) # B特有的股票
print(f"B组合独有: {unique_b}")
# 输出解读:['600000.SH' '601318.SH'],这两只只在B组合中
# 金融应用:策略差异分析,了解不同投资经理的选股偏好6.7 比较运算与布尔索引
# =============================================================================
# 题目:NumPy比较运算与条件筛选
# =============================================================================
# 本任务演示NumPy的比较运算和布尔索引功能
# 场景:根据股价条件筛选股票,找出符合特定标准的股票
# ==================== 创建股价数据 ====================
# 股价数据:5只股票的最新价格
prices = np.array([10.5, 20.3, 15.8, 25.2, 18.6]) # 单位:元
# ==================== 比较运算 ====================
# >:比较运算符,返回布尔数组(True/False)
# 比较每个元素是否大于20
above_20 = prices > 20 # 返回[False, False, False, True, False]
print(f"价格>20元: {above_20}")
# 输出解读:只有第4只股票(25.2元)的价格大于20元,对应位置为True
# 金融应用:筛选高价股、低价股,设置价格阈值
# ==================== 布尔索引 ====================
# 布尔索引:用布尔数组作为索引,提取True位置的元素
# prices > 20:返回布尔数组[False, False, False, True, False]
# prices[布尔数组]:只保留True位置的元素
expensive_stocks = prices[prices > 20] # 筛选价格大于20的股票
print(f"高价股票: {expensive_stocks}")
# 输出解读:[25.2],只有1只股票价格大于20元
# 金融应用:从股票池中筛选符合条件的股票,如PE < 20、市值 > 100亿
# ==================== 多条件筛选 ====================
# &:逻辑与运算符,两个条件同时满足时为True
# (prices >= 15):价格大于等于15
# (prices <= 25):价格小于等于25
# 两个条件用括号括起来,因为&的优先级高于比较运算符
filtered = prices[(prices >= 15) & (prices <= 25)] # 筛选价格在15-25之间的股票
print(f"价格在15-25元之间: {filtered}")
# 输出解读:[15.8, 20.3, 18.6],3只股票的价格在15-25元之间
# 金融含义:价格区间筛选,寻找合理估值的股票
# 应用:价值投资策略,筛选股价处于合理区间的股票
# ==================== 逻辑运算符说明 ====================
# &:逻辑与(AND),两个条件都为True时结果为True
# |:逻辑或(OR),至少一个条件为True时结果为True
# ~:逻辑非(NOT),True变False,False变True
# 注意:必须用括号,因为位运算符优先级高于比较运算符
# 例如:prices > 15 & prices < 25 会报错,应写成 (prices > 15) & (prices < 25)6.8 排序与搜索
# =============================================================================
# 题目:NumPy排序与搜索操作
# =============================================================================
# 本任务演示NumPy的排序和搜索功能
# 场景:对收益率排序,找出最佳和最差表现,以及条件替换
# ==================== 创建无序数据 ====================
# 无序数据:6个交易日的收益率
returns = np.array([0.05, -0.02, 0.03, 0.07, -0.01, 0.04])
# ==================== 排序 ====================
# np.sort():返回排序后的新数组,原数组不变
# 默认升序排列(从小到大)
sorted_returns = np.sort(returns) # 返回排序后的数组
print(f"排序后: {sorted_returns}")
# 输出解读:[-0.02, -0.01, 0.03, 0.04, 0.05, 0.07]
# 金融应用:找出收益率的中位数、分位数,分析收益分布
# ==================== 原地排序 ====================
# .copy():复制数组,避免修改原数组
returns_copy = returns.copy() # 创建returns的副本
# .sort():原地排序,直接修改数组本身,不返回新数组
returns_copy.sort() # 对副本进行原地排序
print(f"原地排序: {returns_copy}")
# 输出解读:returns_copy被排序,returns保持不变
# 性能优势:原地排序不需要额外内存,适用于大数据集
# ==================== argsort:返回排序索引 ====================
# np.argsort():返回排序后的索引数组,而不是排序后的值
# 索引数组表示原始数组中第几个元素应该排在第几位
indices = np.argsort(returns) # 返回索引数组
print(f"\n排序索引: {indices}")
# 输出解读:[1, 4, 2, 5, 0, 3]
# 解释:
# - 索引1的元素(-0.02)应该排在第1位
# - 索引4的元素(-0.01)应该排在第2位
# - 索引0的元素(0.05)应该排在第5位
# - 索引3的元素(0.07)应该排在第6位
# 使用索引数组重建排序后的数组
print(f"按索引重建: {returns[indices]}")
# 金融应用:找出收益率最高和最低的交易日,分析市场时机
# ==================== 搜索:极值位置 ====================
# np.argmax():返回最大值的索引位置
max_idx = np.argmax(returns) # 找出最大值的索引
# np.argmin():返回最小值的索引位置
min_idx = np.argmin(returns) # 找出最小值的索引
print(f"\n最大值位置: {max_idx}, 值: {returns[max_idx]}")
print(f"最小值位置: {min_idx}, 值: {returns[min_idx]}")
# 输出解读:
# - 最大值位置3,值0.07(第4个元素,索引从0开始)
# - 最小值位置1,值-0.02(第2个元素)
# 金融应用:找出最佳和最差交易日,复盘交易决策
# ==================== where:条件选择 ====================
# np.where(condition, x, y):三目运算符
# - condition:布尔条件
# - x:条件为True时返回的值
# - y:条件为False时返回的值
# 将负收益替换为0,正收益保持不变
adjusted = np.where(returns > 0, returns, 0) # 三元运算
print(f"\n负收益调整为0: {adjusted}")
# 输出解读:[0.05, 0, 0.03, 0.07, 0, 0.04]
# 解释:-0.02和-0.01被替换为0,其他保持不变
# 金融应用:止损策略(限制亏损)、收益计算(只计算正收益)6.9 性能优化建议
理论背景:SIMD并行计算
本章开头提到的性能提升并非魔法,NumPy的高性能部分源于SIMD(Single Instruction, Multiple Data)指令集:
- 单指令: 对多个数据执行相同的操作
- 多数据: 一次处理多个数值
- 并行执行: CPU的多个ALU同时工作
例如,计算两个数组的和,传统循环需要逐个元素相加,而SIMD可以一次性处理4个或更多元素(取决于CPU架构)。理解这一底层机制,才能明白下面这些优化建议为什么有效。
# =============================================================================
# 题目:NumPy性能优化最佳实践
# =============================================================================
# 本任务演示NumPy性能优化的关键技巧
# 场景:对比Python循环和NumPy向量化的性能差异,展示优化方法
# ==================== 导入必要的库 ====================
import time # 导入time模块,用于计时
np.random.seed(42) # 固定随机种子,保证每次运行生成相同的随机数组
# ==================== 创建大规模测试数据 ====================
n = 1000000 # 数据规模:100万元素
arr1 = np.random.randn(n) # 生成100万个标准正态分布随机数
arr2 = np.random.randn(n) # 再生成100万个标准正态分布随机数
# np.random.randn(n):生成从标准正态分布(均值0,标准差1)中抽取的n个随机数
# ==================== 1. 使用NumPy内置函数而非Python循环 ====================
# 慢速方法:使用Python循环逐个元素相加
start = time.time() # 记录开始时间
result_slow = [] # 创建空列表存储结果
for i in range(n): # 循环100万次
result_slow.append(arr1[i] + arr2[i]) # 逐个元素相加并添加到列表
slow_time = time.time() - start # 计算耗时(当前时间 - 开始时间)
# 快速方法:使用NumPy向量化运算
start = time.time() # 记录开始时间
result_fast = arr1 + arr2 # 直接对两个数组相加,一次性完成所有运算
fast_time = time.time() - start # 计算耗时
# ==================== 打印性能对比结果 ====================
print(f"循环时间: {slow_time:.4f}秒") # 显示循环耗时
print(f"向量化时间: {fast_time:.4f}秒") # 显示向量化耗时
print(f"加速比: {slow_time/fast_time:.1f}倍") # 计算加速比(循环时间 / 向量化时间)
# 输出解读:加速比通常在50-200倍之间,取决于CPU和数据规模
# 性能优势原因:
# 1. NumPy底层使用C语言,避免了Python解释器开销
# 2. SIMD指令集并行处理多个数据
# 3. 连续内存访问,提高缓存命中率
# 金融应用:大规模数据处理(如tick数据、高频交易数据)必须使用向量化
# ==================== 2. 就地操作节省内存 ====================
arr_large = np.random.randn(10000) # 创建包含10000个元素的数组
# 慢速方法:创建新数组(需要额外内存分配)
result1 = arr_large * 2 # 创建一个新数组存储结果,原数组不变
# 内存消耗:原数组(80KB) + 新数组(80KB) = 160KB
# 快速方法:原地修改(不需要额外内存)
arr_large *= 2 # 使用*=运算符,直接在原数组上修改,不创建新数组
# 内存消耗:原数组(80KB),节省50%内存
# 性能优势:避免内存分配和复制,速度更快
# 注意:就地操作会修改原数组,如果需要保留原数据应先备份
# ==================== 3. 预分配数组 ====================
# 慢速方法:动态增长列表(频繁内存分配)
result = [] # 创建空列表
for i in range(10000): # 循环10000次
result.append(i ** 2) # 每次追加元素,可能触发内存重新分配
result = np.array(result) # 最后转换为NumPy数组
# 性能问题:
# 1. 列表append可能需要多次内存重新分配
# 2. 最终转换需要复制所有数据
# 快速方法:预分配数组(一次性内存分配)
result = np.empty(10000) # 预先分配10000个元素的空数组(未初始化)
for i in range(10000): # 循环10000次
result[i] = i ** 2 # 直接填充预分配的数组,无需重新分配内存
# 性能优势:
# 1. 一次性分配所有内存,避免多次分配
# 2. 直接写入数组,无需列表到数组的转换
# 金融应用:生成大规模模拟数据、历史数据回测6.10 本章小结
要点:
.max()/.min()/.mean()/.var()/.std()直接作用于数组,axis决定沿哪个方向聚合,.cumsum()计算累积和- NumPy 的
var/std默认总体口径(ddof=0),金融分析常用样本口径需显式ddof=1 np.diff()生成相邻差分,是计算日涨跌幅与对数收益率的基础;np.log/np.exp服务对数收益与连续复利- 比较运算返回布尔数组,布尔索引配
&/|/~与np.where()完成条件筛选、替换与止损处理 np.sort()返回新数组、.sort()原地排序;argsort/argmax/argmin负责定位;向量化运算远快于循环
易错点:
- 忘记
ddof=1时,方差、标准差与 Pandas 及多数教材的样本口径不一致 prices > 15 & prices < 25因运算符优先级出错,必须写成(prices > 15) & (prices < 25)and/or/not不能用于数组,数组条件只能用&/|/~print(arr.sort())输出None,因为原地排序没有返回值
6.11 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。
import numpy as np r = np.array([0.03, -0.01, 0.05, 0.02]) print(r.max(), r.min()) print(r.var(), r.var(ddof=1)) print(np.diff(r)) print(np.where(r > 0, r, 0)) s = r.copy() s.sort() print(s) print(r)参考答案(先写下你的预测再点开)
解题思路:逐行推演。
r = [0.03, -0.01, 0.05, 0.02],第 1 个print输出最大值与最小值,得0.05 -0.01。第 2 个求方差:均值为0.0225,离差平方和为 \(0.0075^2+0.0325^2+0.0275^2+0.0025^2=0.001875\),总体方差(ddof=0)除以 \(n=4\) 得0.00046875,样本方差(ddof=1)除以 \(n-1=3\) 得0.000625,两者同印一行,样本口径更大。第 3 个np.diff(r)计算相邻元素差分,得 3 个值[-0.04, 0.06, -0.03]。第 4 个np.where(r > 0, r, 0)把负值替换为 0,得[0.03, 0, 0.05, 0.02](NumPy 打印时 0 显示为0.)。第 5—6 行先copy()再.sort(),s被原地排成升序[-0.01, 0.02, 0.03, 0.05],而r因事先复制不受影响,仍保持原序[0.03, -0.01, 0.05, 0.02]——这正是“副本上原地排序、原数组保序”的安全写法。# 验证脚本:逐行输出七个表达式的求值结果 import numpy as np # 导入NumPy库 r = np.array([0.03, -0.01, 0.05, 0.02]) # 定义收益率数组 print(r.max(), r.min()) # 最大值与最小值 print(r.var(), r.var(ddof=1)) # 总体方差与样本方差 print(np.diff(r)) # 相邻元素差分 print(np.where(r > 0, r, 0)) # 负值替换为0 s = r.copy() # 复制一份用于原地排序 s.sort() # 对副本原地升序排序 print(s) # 输出排序后的副本 print(r) # 原数组不受影响预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
0.05 -0.01 0.00046875 0.000625 [-0.04 0.06 -0.03] [0.03 0. 0.05 0.02] [-0.01 0.02 0.03 0.05] [ 0.03 -0.01 0.05 0.02]回扣本章:对应本章小结“要点”第 2、5 条与“易错点”第 1、4 条——
var/std默认ddof=0、样本口径需显式ddof=1;.sort()原地排序、无返回值。概念辨析:同一组数据
r.var()与r.var(ddof=1)为什么不同?金融风险度量通常采用哪个口径?再说明:np.sort(r)、r_copy = r.copy(); r_copy.sort()、np.argsort(r)三者的返回值各是什么,执行后r本身是否改变。最后解释:简单收益率与对数收益率各自的计算公式,以及对数收益率的时间可加性指什么。参考答案(点开前请先独立完成)
解题思路:逐点作答。第一,两者分母不同:
r.var()按 NumPy 默认的总体口径(ddof=0)除以 \(n\),把手上数据当作全部总体;r.var(ddof=1)按样本口径除以 \(n-1\),是总体方差的无偏估计。样本量越小差异越大(上题实测 0.00046875 对 0.000625)。金融风险度量通常用样本口径(ddof=1),因为我们手中的收益率序列只是历史样本,目的是估计未来的风险,且 Pandas 的std()/var()默认就是ddof=1,不显式调整口径会导致两库结果不一致。第二,本机实测:np.sort(r)返回升序排列的新数组[-0.01, 0.02, 0.03, 0.05],r不变;r_copy = r.copy(); r_copy.sort()的sort()方法返回None,副本被原地排成升序,r仍不变;np.argsort(r)返回的不是数值而是排序索引数组[1, 3, 0, 2](依次指出最小值在索引 1、次小在索引 3……),r也不变——三者都不改变r,区别只在返回值是新数组、None还是索引。第三,简单收益率为 \(r_t = \frac{P_t - P_{t-1}}{P_{t-1}}\),对数收益率为 \(r_t^{log} = \ln\frac{P_t}{P_{t-1}} = \ln P_t - \ln P_{t-1}\);时间可加性指多期对数收益率可直接相加得到跨期对数收益率:\(\ln\frac{P_T}{P_0} = \sum_{t=1}^{T} \ln\frac{P_t}{P_{t-1}}\),而简单收益率多期需连乘 \((1+r_t)\) 计算,不能直接相加。回扣本章:对应本章小结“要点”第 2、3、5 条——方差口径、对数收益率与时间可加性、排序函数的返回值差异。
变式任务(平台任务同型改造):某股票 10 个交易日的收盘价与涨跌幅(%)为
p = np.array([12.50, 12.62, 12.48, 12.55, 12.70, 12.66, 12.58, 12.52, 12.61, 12.65])、r = np.array([0.85, -0.62, 1.35, 0.56, -0.47, 0.31, -0.24, 0.72, 0.32, -0.08])。仿照平台任务,输出最高价与最低价、保留三位小数的平均涨跌幅、保留三位小数的样本方差与样本标准差(ddof=1),再用np.where(r > 0, r, 0)把负涨跌幅替换为 0 后输出。参考答案(点开前请先独立完成)
解题思路:与平台任务同型,差异仅在两处:一是统计涨跌幅方差与标准差时显式加
ddof=1改用样本口径;二是增加np.where(r > 0, r, 0)把负涨跌幅替换为 0。最高价 12.70、最低价 12.48(NumPy 打印时 12.70 与 12.48 分别显示为12.7与12.48,末尾的 0 被省略);10 日涨跌幅之和为 2.70,平均涨跌幅0.27(%);样本方差0.391、样本标准差0.625;替换后数组中 -0.62、-0.47、-0.24、-0.08 四个负值变为 0。# 变式程序:10日行情的统计量计算与负涨跌幅替换 import numpy as np # 导入NumPy库 p = np.array([12.50, 12.62, 12.48, 12.55, 12.70, 12.66, 12.58, 12.52, 12.61, 12.65]) # 10日收盘价 r = np.array([0.85, -0.62, 1.35, 0.56, -0.47, 0.31, -0.24, 0.72, 0.32, -0.08]) # 对应涨跌幅(%) print(p.max(), p.min()) # 输出最高价与最低价 print(r.mean().round(3)) # 平均涨跌幅,保留三位小数 print(r.var(ddof=1).round(3), r.std(ddof=1).round(3)) # 样本方差与样本标准差(ddof=1) print(np.where(r > 0, r, 0)) # 负涨跌幅替换为0后输出预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
12.7 12.48 0.27 0.391 0.625 [0.85 0. 1.35 0.56 0. 0.31 0. 0.72 0.32 0. ]注意:以上为本题变式的独立代码;列表 6.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“要点”第 1、2、4 条——
.max()/.min()/.mean()/.var()/.std()直接作用于数组,金融分析须显式ddof=1,np.where()完成条件替换。动手验证:设组合A 持仓
['600036.SH', '000858.SZ', '601318.SH', '600519.SH'],组合B 持仓['600036.SH', '000858.SZ', '600000.SH', '000002.SZ']。分别用np.intersect1d、np.union1d、np.setdiff1d输出共同持仓、全部持仓与各自独有持仓;再用布尔索引筛选出p中介于 12.50 与 12.62 之间(含端点)的价格。参考答案(点开前请先独立完成)
解题思路:集合运算部分,两组合各有 4 只股票、共同持有 2 只(
000858.SZ、600036.SH),交集 2 只、并集 \(4+4-2=6\) 只(结果按代码升序排列),np.setdiff1d(a, b)给出 A 独有的 2 只(600519.SH、601318.SH),np.setdiff1d(b, a)给出 B 独有的 2 只(000002.SZ、600000.SH)。布尔筛选部分用(p >= 12.50) & (p <= 12.62),注意两个条件必须各自加括号再用&连接(&的优先级高于比较运算符),含端点故用>=与<=;p中满足条件的有 12.50、12.62、12.55、12.58、12.52、12.61 共 6 个价格。# 验证脚本:两组合持仓的集合运算与价格区间筛选 import numpy as np # 导入NumPy库 portfolio_a = np.array(['600036.SH', '000858.SZ', '601318.SH', '600519.SH']) # 组合A持仓 portfolio_b = np.array(['600036.SH', '000858.SZ', '600000.SH', '000002.SZ']) # 组合B持仓 print(np.intersect1d(portfolio_a, portfolio_b)) # 共同持仓(交集) print(np.union1d(portfolio_a, portfolio_b)) # 全部持仓(并集,去重) print(np.setdiff1d(portfolio_a, portfolio_b)) # A组合独有持仓 print(np.setdiff1d(portfolio_b, portfolio_a)) # B组合独有持仓 p = np.array([12.50, 12.62, 12.48, 12.55, 12.70, 12.66, 12.58, 12.52, 12.61, 12.65]) # 10日收盘价 print(p[(p >= 12.50) & (p <= 12.62)]) # 区间筛选(含端点),括号不可省预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
['000858.SZ' '600036.SH'] ['000002.SZ' '000858.SZ' '600000.SH' '600036.SH' '600519.SH' '601318.SH'] ['600519.SH' '601318.SH'] ['000002.SZ' '600000.SH'] [12.5 12.62 12.55 12.58 12.52 12.61]回扣本章:对应本章小结“要点”第 4 条与“易错点”第 2 条——布尔索引配
&/|/~完成多条件筛选,条件必须各自加括号。